SPDX-FileCopyrightText: 2026 Marina Hinojal Vicente & Yu-Hsiang Hu SPDX-FileCopyrightText: 2026 AlICe laboratory https://alicelab.be
SPDX-License-Identifier: GPL-3.0-or-later
import math
import os
import random
import bmesh
import bpy
os.system("cls")def clean():
objects_to_keep = ["Camera__Iso"]
for obj in bpy.data.objects:
if obj.name in objects_to_keep:
continue
bpy.data.objects.remove(obj, do_unlink=True)
for mesh in bpy.data.meshes:
if not mesh.users:
bpy.data.meshes.remove(mesh)
clean()
total_faces = range(289)
faces_of_flute = [
28,
29,
30,
31,
32,
45,
46,
47,
48,
49,
77,
92,
93,
94,
95,
109,
110,
111,
112,
126,
127,
129,
175,
176,
177,
178,
192,
193,
194,
195,
207,
222,
223,
224,
225,
239,
240,
241,
242,
256,
257,
259,
]
opposite_faces = [i for i in total_faces if i not in faces_of_flute]def Delete_faces(_Mesh, faces_to_delete):
myObj = bpy.data.objects[_Mesh]
myObjGeom = myObj.data
myBmesh = bmesh.new()
myBmesh.from_mesh(myObjGeom) myBmesh.faces.ensure_lookup_table()
max_faces = len(myBmesh.faces)
_Faces = [myBmesh.faces[i] for i in faces_to_delete]
bmesh.ops.delete(myBmesh, geom=_Faces, context="FACES")edge clean up
edges_to_remove = [e for e in myBmesh.edges if len(e.link_faces) == 0]
bmesh.ops.delete(myBmesh, geom=edges_to_remove, context="EDGES") myBmesh.to_mesh(myObjGeom)
myObj.data.update()
myBmesh.free()def generateGrid(_GridName, Obj_num, z_location):
bpy.ops.mesh.primitive_grid_add(
x_subdivisions=17,
y_subdivisions=17,
location=(0, 0, z_location),
size=10,
)
bpy.context.object.name = _GridName
Obj_num = bpy.data.objects[_GridName]def _Modifiers_Smooth(_Mesh):
myObj = bpy.data.objects[_Mesh]
smooth_it = myObj.modifiers.new(name="smooth_it", type="SUBSURF")
smooth_it.levels = 2
smooth_it.boundary_smooth = "PRESERVE_CORNERS"
bpy.ops.object.modifier_apply(modifier=smooth_it.name)def _Modifiers_Bevel(_Mesh, _width, _seg):
myObj = bpy.data.objects[_Mesh]
bevel_it = myObj.modifiers.new(name="bevel_it", type="BEVEL")
bevel_it.width = _width
bevel_it.segments = _segdef Extrude_Z(_Mesh, _min, _max):
myObj = bpy.data.objects[_Mesh]activate target obj
bpy.context.view_layer.objects.active = myObjenter edit mode
bpy.ops.object.mode_set(mode="EDIT")select all surfaces
bpy.ops.mesh.select_mode(type="FACE")
bpy.ops.mesh.select_all(action="SELECT")extrude via z axis
_thickness = random.uniform(_min, _max)
bpy.ops.mesh.extrude_region_move(
TRANSFORM_OT_translate={"value": (0, 0, _thickness)}
)object mode
bpy.ops.object.mode_set(mode="OBJECT")def Remodel_Proportional(_Mesh, _CenterIndices, _radius, _min_h, _max_h):
myObj = bpy.data.objects[_Mesh]
for v_index in _CenterIndices:to have the x,y coordinate of every verts in CenterIndices
center_vert = myObj.data.vertices[v_index]
center_x = center_vert.co.x
center_y = center_vert.co.ydefine a variation to have a random height
target_wave_height = random.uniform(_min_h, _max_h)create the wavy surface
for v in myObj.data.vertices:Pythagoras’ theorem(a^2 + b^2 = c^2)
dist = math.sqrt((v.co.x - center_x) ** 2 + (v.co.y - center_y) ** 2)useing _radius to define the scope moving when dist=_radius, dont’t move when dist=0, move the most
if dist < _radius:
falloff = (math.cos(math.pi * (dist / _radius)) + 1.0) / 2.0
v.co.z += target_wave_height * falloffdef Move_Independently(_Mesh, min_z, max_z):
myObj = bpy.data.objects[_Mesh]
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.separate(type="LOOSE") # separate independent mesh
bpy.ops.object.mode_set(mode="OBJECT")move separated parts independently
separated_parts = bpy.context.selected_objects
for obj in separated_parts:
z_offset = random.uniform(min_z, max_z)
obj.location.z += z_offsetjoin the mesh again
bpy.context.view_layer.objects.active = separated_parts[0]
bpy.ops.object.join()
bpy.context.active_object.name = _Meshdef _ScaleUp(_Mesh, min_scale, max_scale):
original_obj = bpy.data.objects.get(_Mesh)
original_obj.select_set(True)
bpy.context.view_layer.objects.active = original_objduplicate
bpy.ops.object.duplicate()
duplicated_obj = bpy.context.active_objectseparate into independent mesh
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.mesh.separate(type="LOOSE")
bpy.ops.object.mode_set(mode="OBJECT")reset the origin of each geometry
bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY", center="MEDIAN")
separated_parts = bpy.context.selected_objectsscale up each part
for i in range(len(separated_parts)):
obj = separated_parts[i]
obj.name = f"{_Mesh}_scaleup_part_{i + 1}"
s = random.uniform(min_scale, max_scale)
obj.scale = (s, s, s)def Clean_Mesh(_Mesh):
myObj = bpy.data.objects.get(_Mesh)
bpy.context.view_layer.objects.active = myObj
bpy.ops.object.mode_set(mode="EDIT")remove overlapped vertices
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.mesh.remove_doubles(threshold=0.001)delete interior Faces
bpy.ops.mesh.select_all(action="DESELECT")
bpy.ops.mesh.select_interior_faces()
bpy.ops.mesh.delete(type="FACE")reset normals
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.mesh.normals_make_consistent(inside=False)
bpy.ops.object.mode_set(mode="OBJECT")def boolean_Difference(operand_A, operand_B):
Boolean_boxes = operand_A.modifiers.new(name="my_Boolean", type="BOOLEAN")
Boolean_boxes.operation = "DIFFERENCE"
Boolean_boxes.object = operand_B
Boolean_boxes.solver = "EXACT"def boolean_Intersect(operand_A, operand_B):
Boolean_boxes = operand_A.modifiers.new(name="my_Boolean", type="BOOLEAN")
Boolean_boxes.operation = "INTERSECT"
Boolean_boxes.object = operand_B
Boolean_boxes.solver = "EXACT"Start of the Keyboard -----------------------------------------------------------
FacesToMove_Top = [0, 1, 2, 17, 18, 19, 34, 35, 36]
FacesToMove_Bottom = [0, 14, 20]
CentersToMove_Top = [
0,
314,
17,
]
CentersToMove_Bottom = [
306,
9,
323,
]
generateGrid("keyboard_top", "kb_1", 4.5)
Remodel_Proportional("keyboard_top", CentersToMove_Top, 6.0, 2, 4)
Remodel_Proportional("keyboard_top", CentersToMove_Bottom, 6.0, -3, -3)
_Modifiers_Smooth("keyboard_top")
generateGrid("keyboard_bottom", "kb_2", 4.3)
Remodel_Proportional("keyboard_bottom", CentersToMove_Bottom, 6.0, -3, -6)
Remodel_Proportional("keyboard_bottom", CentersToMove_Top, 6.0, 1, 2)
_Modifiers_Smooth("keyboard_bottom")----- Bridge the edge
obj_top = bpy.data.objects["keyboard_top"]
obj_bottom = bpy.data.objects["keyboard_bottom"]select the two surfaces
bpy.ops.object.select_all(action="DESELECT")
obj_top.select_set(True)
obj_bottom.select_set(True)joing the two surface before bridging
bpy.context.view_layer.objects.active = obj_top
bpy.ops.object.join()change the name
obj_top.name = "Keyboard_Solid"Enter Edit Mode
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.select_mode(type="EDGE")
bpy.ops.mesh.select_all(action="DESELECT")Select Non-manifold edges
bpy.ops.mesh.select_non_manifold()Bridge them
bpy.ops.mesh.bridge_edge_loops()Recalculate Normals
bpy.ops.mesh.normals_make_consistent(inside=False)Object Mode
bpy.ops.object.mode_set(mode="OBJECT")----- Carve the keyboard
generateGrid("Void", "obj_6", -5)
Delete_faces("Void", opposite_faces)
Extrude_Z("Void", 20, 20)
_Modifiers_Bevel("Void", 0.2, 20)
_ScaleUp("Void", 1.0, 1.2)
Clean_Mesh("Keyboard_Solid") # To prevent from non-manifold after boolean
boolean_Difference(
bpy.data.objects["Keyboard_Solid"], bpy.data.objects["Void_scaleup_part_1"]
)
boolean_Difference(
bpy.data.objects["Keyboard_Solid"], bpy.data.objects["Void_scaleup_part_2"]
)
boolean_Difference(
bpy.data.objects["Keyboard_Solid"], bpy.data.objects["Void_scaleup_part_3"]
)
boolean_Difference(
bpy.data.objects["Keyboard_Solid"], bpy.data.objects["Void_scaleup_part_4"]
)
bpy.data.objects["Void"].hide_set(True)
for i in range(4):
bpy.data.objects[f"Void_scaleup_part_{i + 1}"].hide_set(True)
bpy.ops.mesh.primitive_cube_add(size=1, location=(0, 0, 5), scale=(11, 11, 10))
boundary_obj = bpy.context.active_object
boundary_obj.name = "boundary"
boundary_obj.data.name = "boundary_mesh"
boolean_Intersect(bpy.data.objects["Keyboard_Solid"], bpy.data.objects["boundary"])
bpy.data.objects["boundary"].hide_set(True)End of the Keyboard -------------------------------------------------------------
Start of the Flute --------------------------------------------------------------
generateGrid("Flute", "obj_4", 7)
Remodel_Proportional("Flute", CentersToMove_Top, 6.0, 1, 5)
Delete_faces("Flute", opposite_faces)
_Modifiers_Smooth("Flute")
Move_Independently("Flute", -8, -2)
Extrude_Z("Flute", 2.5, 5.5)
_Modifiers_Bevel("Flute", 0.2, 20)— Add red color
RedColor = bpy.data.materials.new("Red")
RedColor.diffuse_color = (1.0, 0.0, 0.0, 1.0) # (R, G, B, Transparency)
bpy.data.objects["Flute"].data.materials.append(RedColor)End of the Flute ----------------------------------------------------------------
Start of the Beat ---------------------------------------------------------------
def _beat_a(_pos, _dim, _name):
bpy.ops.mesh.primitive_cube_add(size=1, location=_pos, scale=_dim)
bpy.context.object.name = _name
bpy.context.object.data.name = _name + "_mesh"def _beat_b(_pos, _dim, _name):
bpy.ops.mesh.primitive_cube_add(size=1, location=_pos, scale=_dim)
obj_beat = bpy.context.active_object
obj_beat.name = _name
obj_beat.data.name = _name + "_mesh"
bpy.ops.object.duplicate(linked=False)
obj_copy = bpy.context.object
obj_copy.name = _name + "_copy"
obj_copy.rotation_euler[2] = math.radians(90)
myObj = bpy.data.objects[_name]
obj_beat.select_set(True)
obj_copy.select_set(True)
bpy.context.view_layer.objects.active = obj_beat
bpy.ops.object.join()define the start point and step, and use 10/34 to make sure the center of every beat is in grid
start_val = (10 / 34) * 5 - 5 # '-5' because the grid center is (0,0)
step_val = (10 / 34) * 8Add a factor to control the height of the beat beat(0,0) and beat(3,3) are the biggest, decreasing toward center
max_abs_sum = abs(start_val * 2)
for rowY in range(4):
for rowX in range(4):calculate the real xy location
realX = start_val + (rowX) * step_val
realY = start_val + (rowY) * step_val
min_height = random.uniform(10.0, 10.0)
max_height = random.uniform(10.0, 10.0)factor based on abs(realX + realY)
factor = abs(realX + realY) / max_abs_sumkeep a minimum height and add a value based on the factor
base_height = min_height + (max_height - min_height) * factorTo have a slightly variation
final_height = random.uniform(base_height - 0.5, base_height + 0.5)
if (rowX + rowY) % 2 == 0:
rSize_a = random.uniform(0.4, 0.5)
name_a = "Beat_a"
_beat_a(
(realX, realY, final_height / 2),
(rSize_a, rSize_a, final_height),
name_a,
)
else:
name_b = "Beat_b"
rSize_b = random.uniform(0.4, 0.5)
_beat_b(
(realX, realY, final_height / 2),
(rSize_b, rSize_b / 5, final_height),
name_b,
)End of the Beat ----------------------------------------------------------------
Start of the Tension and Atmoshere -----------------------------------------------
def create_line_between(pA, pB, _rmin, _rmax):
dx = pB[0] - pA[0]
dy = pB[1] - pA[1]
dz = pB[2] - pA[2]
dist = math.sqrt(dx**2 + dy**2 + dz**2)
mid_x = (pA[0] + pB[0]) / 2.0
mid_y = (pA[1] + pB[1]) / 2.0
mid_z = (pA[2] + pB[2]) / 2.0
_radius = random.uniform(_rmin, _rmax)
bpy.ops.mesh.primitive_cylinder_add(
radius=_radius,
depth=dist,
location=(mid_x, mid_y, mid_z),
)
phi = math.atan2(dy, dx)
theta = math.acos(dz / dist)
bpy.context.object.rotation_euler[1] = theta
bpy.context.object.rotation_euler[2] = phi
coords_up = []
coords_down = []bubbles above the plane
for _ in range(15):
pos_x = random.uniform(-6.0, 6.0)
pos_y = random.uniform(-6.0, 6.0)
pos_z = random.uniform(5.0, 12.0)
_r_radius = random.uniform(0.25, 1.0)
coords_up.append((pos_x, pos_y, pos_z))
bpy.ops.mesh.primitive_uv_sphere_add(
radius=1,
location=(pos_x, pos_y, pos_z),
scale=(_r_radius, _r_radius, _r_radius),
)bubbles below the plane
for _ in range(10):
pos_x = random.uniform(-6.0, 6.0)
pos_y = random.uniform(-6.0, 6.0)
pos_z = random.uniform(-2.0, 5.0)
_r_radius = random.uniform(0.25, 1.0)
coords_down.append((pos_x, pos_y, pos_z))
bpy.ops.mesh.primitive_uv_sphere_add(
radius=1,
location=(pos_x, pos_y, pos_z),
scale=(_r_radius, _r_radius, _r_radius),
)change the sequence of the list to make it more random
random.shuffle(coords_up)
random.shuffle(coords_down)connect the bubbles three times
for i in range(3):to have 2 bubbles above,i and i+1
p1_up = coords_up[i * 2]
p2_up = coords_up[i * 2 + 1]to have 2 bubbles below
p3_down = coords_down[i]connect these points
create_line_between(p1_up, p2_up, 0.075, 0.1)
create_line_between(p2_up, p3_down, 0.075, 0.1)
create_line_between(p3_down, p1_up, 0.075, 0.1)End of the Tension and Atmoshere ------------------------------------------------